#include "process_management.h"

/* global variables */

/* this list contains all the PCBs */
pcb_list processes;

/* function bodies */

void initialize_processes_list(){
  processes = NULL;
}

/* creates a new process. Returns SUCCESS, UNKNOWN_PROGRAM, WRONG_PRIORITY or OUT_OF_MEMORY. */
int32_t create_process(uint32_t priority, char* program_name, int32_t parent,int32_t parameters[4], int reschedule){
  int32_t j, program_id, pid;
  uint32_t entry_point, stack;
  PCB* pcb;
  registers_t* regs;

  if(priority == 0 || priority > MAX_PRIORITY)
    //this priority is not possible in this system
    return WRONG_PRIORITY;

  pcb = malloc(sizeof(PCB), SYSTEM_ID);
  if(pcb == NULL)
    //out of memory
    return OUT_OF_MEMORY;
  initialize_pcb(pcb);
  /* let's get the entry point of the program */
  program_id = get_program_information(program_name,&entry_point);
  if(program_id == UNKNOWN_PROGRAM){
    free(pcb, SYSTEM_ID);
    return UNKNOWN_PROGRAM; 
  }
  /* we register the information got above */
  pcb_set_program_id(pcb, program_id);
  pcb_set_epc(pcb, entry_point); 
  pcb_set_priority(pcb, priority);
  pcb_set_state(pcb, READY_STATE);
  pcb_set_status(pcb, 0);
  pcb_set_parent(pcb, parent);
  pcb_set_ra(pcb,(uint32_t)quitter_succes); //we inject the return value of the UP to make sure it'll terminate correctly
  // now, we have to allocate a stack. The stack size is fixed.
  stack = (uint32_t)malloc(STACK_SIZE*sizeof(int),SYSTEM_ID);
  if((void*)stack == NULL){
    //out of memory. Don't forget to free the PCB
    free(pcb, SYSTEM_ID);
    return OUT_OF_MEMORY;
  }
  pcb_set_sp(pcb,(uint32_t)((int*)stack + (STACK_SIZE - 5)));
  pcb_set_stack(pcb,stack);  /*the stack grows towards the lower addresses, that's why we have to add STACK_SIZE. We substract 4 because the compiler stores the parameters above the functions (so, we must leave some space for them). */
  /* "injects" the parameters directly in the a registers. Either NULL or an array of four integers must be passed. */
  if(parameters != NULL){
    regs = pcb_get_registers(pcb);
    for(j = 0; j < 4; j ++)
      //regs->a_reg[j] = parameters[j];
      pcb->registers.a_reg[j] = parameters[j];
  }
  if(pcb_list_add(&processes, pcb,&pid) == OUT_OF_MEMORY){
    //let's remove the pcb to create some place
    free_pcb(pcb);
    return OUT_OF_MEMORY;
  }
  if(insert_ready_queue(pcb,reschedule) == OUT_OF_MEMORY){
    //let's remove the pcb to create some place
    pcb_list_remove(&processes,pcb);
    return OUT_OF_MEMORY;
  }
  return pid;
}

int32_t remove_process(int32_t pid, int32_t return_value){
  PCB* pcb;
  pcb = get_process_by_pid(pid);
  if(pcb != NULL) {
    /* call process supervision to notify and delete */
    return remove_process_pcb(pcb,return_value);
  }
  return FAILURE;
}

/* Removes a PCB from the system. Returns SUCCESS, FAILURE or OUT_OF_MEMORY. */
int32_t remove_process_pcb(PCB* pcb, int32_t return_value){
  uint32_t state = pcb_get_state(pcb);
  int32_t r_st, r_adopt;
  //saves the return value
  pcb_set_status(pcb, return_value);
  //removes the PCB from the different places it may be
  switch(state){
    case ZOMBIE_STATE:
      //the process has already be terminated but stayed in the system as a zombie, nothing more can be done
      return SUCCESS;
    case RUNNING_STATE:
    case READY_STATE:
      if(remove_ready_queue(pcb) == FAILURE) //removes this PCB from the ready queue
        return FAILURE;
      break;
    case WAITING_IO_UART_STATE:
      if(remove_uart_queue(pcb) == FAILURE)
        return FAILURE;
      break;
    case DOING_IO_UART_STATE:
      if(end_of_io() == FAILURE || remove_ready_queue(pcb) == FAILURE)
        return FAILURE;
      break;
    case DELAYED_STATE:
      if(remove_delayed(pcb) == FAILURE)
        return FAILURE;
      break;
    default:
      ; //blocked or waiting for child termination, nothing special to do
  }
  /* removes any allocated memory the process did not free. Since messages are allocated on behalf on their receiver, any message not freed, read or not, is freed now. */
  free_all_pid(pcb_get_pid(pcb));
  
  /*
     now, the PCB has been removed from everywhere except the process table.
     We check if it has some children that needs to be adopted by the system
     */
  r_adopt = adopt_children(pcb);

  /* signal termination and clean up supervisors and supervisees. We keep the return value and continue to free the more things possible.  */
  r_st = signal_termination(pcb, return_value);
  /*
     Now, let's check if its parent is waiting for the return value. Otherwise, the process will become a zombie. This function also deletes pcb if possible.
     */
  int32_t r = pass_return_value(pcb,return_value);
  if(r_st == OUT_OF_MEMORY || r_st == FAILURE){
    return r_st;
  }
  if(r_adopt == FAILURE){
    return FAILURE;
  }
  return r;
}

/* looks for pcb's children and changes their parents to SYSTEM_ID. Returns SUCCESS or FAILURE. */
int32_t adopt_children(PCB* pcb){
  PCB* p;
  int32_t retv = SUCCESS;
  pcb_list current = processes;
  while(current != NULL){
    p = current->pcb;
    if(pcb_get_parent(p) == pcb_get_pid(pcb)){
      // we found a child
      if(pcb_get_state(p) == ZOMBIE_STATE){
	/* OMG ! A zombie ! Run !
	   Now that its parent is going to quit, there is no need for this zombie anymore */
	if(delete_pcb(p) == FAILURE)//one bullet, right in the head...
	  retv = FAILURE;
      }else
	pcb_set_parent(p, SYSTEM_ID);
    }
    current = current -> next;
  }
  return retv;
}

/* frees pcb and its PID */
int32_t delete_pcb(PCB* pcb){
  return pcb_list_remove(&processes,pcb);
}

/* passes return_value to pcb's parent if it is waiting for it. Otherwise, pcb becomes a zombie. Returns SUCCESS in case of success, FAILURE otherwise. */
int32_t pass_return_value(PCB* pcb, int32_t return_value){
  int32_t parent = pcb_get_parent(pcb);
  PCB* parent_pcb;
  if(parent != SYSTEM_ID){
    //the parent of this process is another process
    parent_pcb = get_process_by_pid(parent);
    if(parent_pcb == NULL)
      //this should not be possible but let's be careful
      return FAILURE;
    registers_t* parent_regs = pcb_get_registers(parent_pcb);
    if(pcb_get_state(parent_pcb) == WAITING_CHILD_TERM_STATE && pcb_get_waiting_for(parent_pcb) == pcb_get_pid(pcb)){
      /* the parent was waiting for the termination of this child
         the return_value parameter has to be passed using the registers of the parent */
      *((int32_t*)parent_regs->a_reg[1]) = return_value;
      wake_up_pcb(parent_pcb); //the parent can now resume
    }else{
      //the process becomes a zombie
      pcb_set_state(pcb, ZOMBIE_STATE);
      return SUCCESS; //we can't deallocate the PCB now
    }
  }
  return delete_pcb(pcb); //this PCB is no longer used, that does the pid recycling as well 
}

/* returns an array containing all the pid in the system and their number in length */
int32_t* get_all_pid(int32_t* length, int32_t owner){
  return pcb_list_get_all_pid(processes,length,owner);
}

PCB* get_process_by_pid(int32_t pid){
  pcb_list current = processes;
  for(current = processes; current != NULL && pcb_get_pid(current->pcb) < pid; current = current->next);
  if(current != NULL && pcb_get_pid(current->pcb) == pid)
    //found it !
    return current->pcb;
  // not found
  return NULL;
}

/* changes the priority of the process to priority */
int32_t change_priority(int32_t pid, int32_t priority) {
  if(priority == 0 || priority > MAX_PRIORITY)
    //not possible in this system
    return WRONG_PRIORITY;
  PCB* target = get_process_by_pid(pid);
  if(target != NULL){
    uint32_t state = pcb_get_state(target);
    if(state == RUNNING_STATE || state == READY_STATE){
      //need to rearrange the ready queue
      if(remove_ready_queue(target) == FAILURE)
	return FAILURE;
      pcb_set_priority(target, priority);
      return insert_ready_queue(target,1);
    }else
      pcb_set_priority(target, priority);
    return SUCCESS;
  }
  return FAILURE;
}

/* blocks the process pid. It is only possible to block a process ready or running (blocking a delayed process would not make much sense) */
int32_t block(int32_t pid){
  PCB* target = get_process_by_pid(pid);
  if(target != NULL)
    return block_pcb(target);
  return FAILURE;
}

/* blocks the process pointed by target. It is only possible to block a process ready, running or delayed. A delayed process does not resume its waiting when waking up. */
int32_t block_pcb(PCB* target){
  int32_t state = pcb_get_state(target);
  if(state == RUNNING_STATE || 
      state == READY_STATE ||
      state == DELAYED_STATE){
    //the process is not already blocked
    pcb_set_state(target, BLOCKED_STATE);
    //if the process is delayed, we need to remove from the delayed queue
    if(state == DELAYED_STATE){
      if(remove_delayed(target) == FAILURE)
        return FAILURE;
    } else {
      //the process is in the ready queue
      if(remove_ready_queue(target) == FAILURE){
        return FAILURE; //failure
      }
    }
    return SUCCESS;
  }
  return FAILURE;
}

/* wakes the process pid up. This function is only accessed through the API */
int32_t wake_up(int32_t pid){
  PCB* target = get_process_by_pid(pid);
  if(target != NULL){
    uint32_t state = pcb_get_state(target);
    //prevents a process from waking up another one doing I/O or waiting for a child via a syscall
    if(state == WAITING_IO_UART_STATE || state == DOING_IO_UART_STATE || state == WAITING_CHILD_TERM_STATE)
      return FAILURE;
    return wake_up_pcb(target);
  }
  return FAILURE;
}

/* wakes the process pointed by target up.
   Returns 1 in case of success, 0 otherwise.
*/
int32_t wake_up_pcb(PCB* target){ 
  uint32_t state = pcb_get_state(target);
  if(!(state  == READY_STATE || state == RUNNING_STATE || state == ZOMBIE_STATE)){
    if(state == DELAYED_STATE)
      //we need to remove the process from the delayed array
      if(remove_delayed(target) == FAILURE)
	return FAILURE;
    pcb_set_state(target,READY_STATE);
    return insert_ready_queue(target,1);
  }
  return FAILURE;
}


/* delays the process pid during n ms.
   Returns SUCCESS if the process was successfully delayed, FAILURE otherwise.
*/
int32_t delay(int32_t pid, uint32_t n){
  PCB* target = get_process_by_pid(pid);
  if(target != NULL)
    return delay_pcb(target, n);
  return FAILURE;
}

/* delays the process pointed by target during n ms. Delay an already delayed process changes the amount of time it has to wait to n
   Returns SUCCESS if the process was sucessfully delayed, FAILURE otherwise. */
int32_t delay_pcb(PCB* target, uint32_t n){
  int state = pcb_get_state(target);
  if(state == RUNNING_STATE || 
     state == READY_STATE ||
     state == DELAYED_STATE){
    pcb_set_delay(target,n);
    //if the process is already delayed, that's all we need to do. Otherwise:
    if(state != DELAYED_STATE){
      pcb_set_state(target,DELAYED_STATE);
      if(insert_delayed(target) == OUT_OF_MEMORY)
	//we couldn't insert the pcb in the delayed array, we cannot continue
	return FAILURE;
      if(remove_ready_queue(target) == FAILURE)
	return FAILURE;
    }
    //success !
    return SUCCESS;
  }
  return FAILURE;
}

/* Returns the priority of the process pid or FAILURE if it doesn't exist */
int32_t get_priority(int32_t pid){
    PCB* pcb = get_process_by_pid(pid);
    if(pcb == NULL)
      return FAILURE;
    return pcb_get_priority(pcb);
}

/* Returns the program name of the program executed by pid or NULL if an error occurs. */
char* get_program(int32_t pid, int32_t owner){
  PCB* pcb = get_process_by_pid(pid);
  if(pcb == NULL)
    return NULL;
  return get_program_name(pcb_get_program_id(pcb), owner);
}

/* returns the state of the process pid or FAILURE in case of failure */
int32_t get_state(int32_t pid){
  PCB* pcb = get_process_by_pid(pid);
  if(pcb == NULL)
    return FAILURE;
  return pcb_get_state(pcb);
}

/* returns the parent's pid of pid or FAILURE in case of failure */
int32_t get_parent(int32_t pid){
  PCB* pcb = get_process_by_pid(pid);
  if(pcb == NULL)
    return FAILURE;
  return pcb_get_parent(pcb);
}

/* checks if the process pid has terminated. If it has, its return value is copied in status and the process is deleted. Otherwise, the caller is blocked until such an event occurs. Returns SUCCESS if no error happened (being blocked is NOT an error) or FAILURE otherwise. */
int32_t wait(int32_t pid, int32_t* status){
  PCB* pcb = get_process_by_pid(pid);
  PCB* caller = get_running();
  if(pcb == NULL || caller == NULL)
    //no such process in the system !
    return FAILURE;
  if(pcb_get_parent(pcb) != pcb_get_pid(caller))
    //the pid is not that of a caller's child
    return FAILURE;
  
  //has the child terminated yet? If it has, it's a zombie now 
  if(pcb_get_state(pcb) == ZOMBIE_STATE){
    //the return value is stored in its pcb
    *status = pcb_get_status(pcb);
    //the process can be deleted now
    delete_pcb(pcb); //rest in peace
  }else{
    //the child is not done yet, we have to wait
    block_pcb(caller);
    pcb_set_state(caller, WAITING_CHILD_TERM_STATE);
    pcb_set_waiting_for(caller, pid);
  }
  //We are sure everything is alright. The call cannot fail anymore. So, we can do the return, even if the caller has been blocked
  return SUCCESS;
}

					
